﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.FxCop.Sdk;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using FxCopContrib.TestHarness.Properties;
using System.Globalization;
using System.IO;

namespace FxCopContrib.TestHarness
{
    public class FxCopTest
    {
        public static FxCopTest<TRule> ForRule<TRule>()
            where TRule : BaseIntrospectionRule, new()
        {
            return new FxCopTest<TRule>();
        }
        
        public static FxCopTest<TRule> ForRule<TRule>(TRule rule)
            where TRule : BaseIntrospectionRule, new()
        {
            return new FxCopTest<TRule>().ForRule(rule);
        }
    }

    public class FxCopTest<TRule>
        where TRule : BaseIntrospectionRule, new()
    {
        TRule _rule;
        AssemblyUnderTest _assembly;
        List<ProblemPrototype> expected = new List<ProblemPrototype>();
        List<ProblemPrototype> notExpected = new List<ProblemPrototype>();
        bool conclusive = false;

        internal FxCopTest() { }

        public FxCopTest<TRule> ForRule(TRule rule)
        {
            _rule = rule;
            return this;
        }

        public FxCopTest<TRule> LoadExternal()
        {
            TryLoadCodeByName();
            if (_assembly == null)
            {
            }
            return this;
        }

        public FxCopTest<TRule> OnCode(string code)
        {
            _assembly = new AssemblyUnderTest(code);
            return this;
        }

        public FxCopTest<TRule> OnType(string code, params string[] namespaces)
        {
            code = string.Format(CultureInfo.InvariantCulture, Resources.TypeTemplate, GenerateNameSpaces(namespaces), code);
            _assembly = new AssemblyUnderTest(code);
            return this;
        }

        public FxCopTest<TRule> OnMethod(string code, params string[] namespaces)
        {

            code = string.Format(CultureInfo.InvariantCulture, Resources.MethodTemplate, GenerateNameSpaces(namespaces), code);
            _assembly = new AssemblyUnderTest(code);
            return this;
        }

        public string GenerateNameSpaces(string[] namespaces)
        {
            return string.Join("", namespaces.Select(n => "using " + n + ";\r\n"));
        }

        public FxCopTest<TRule> WithReferencesTo(params Type[] types)
        {
            _assembly.AddReferences(types);
            return this;
        }

        public FxCopTest<TRule> WithReferenceTo(Assembly assembly)
        {
            _assembly.AddReference(assembly);
            return this;
        }

        public FxCopTest<TRule> WillFind(params ProblemPrototype[] problems)
        {
            expected.AddRange(problems);
            return this;
        }

        public FxCopTest<TRule> WillNotFind(params ProblemPrototype[] problems)
        {
            notExpected.AddRange(problems);
            return this;
        }

        public FxCopTest<TRule> Conclusive()
        {
            conclusive = true;
            return this;
        }

        public void Verify()
        {
            _rule = new TRule();

            if (_assembly == null)
            {
                throw new InvalidOperationException("No code specified and no test class found");
            }

            FxCopRunner runner = new FxCopRunner(_rule, _assembly);
            runner.Run();

            List<Problem> found = new List<Problem>(_rule.Problems);

            foreach (var problem in expected)
            {
                var matching = found.FindAll(p => problem.Matches(p));
                if (matching.Count == problem.Count)
                {
                    found.RemoveAll(p => matching.Contains(p));
                }
                Assert.AreEqual(problem.Count, matching.Count, "Expected to find rule '{0}:{1}' once, but found {2}", _rule.CheckId, _rule.Name, matching.Count);
            }

            foreach (var problem in notExpected)
            {
                var matching = found.FindAll(p => problem.Matches(p));
                Assert.AreEqual(0, matching.Count, "Did not expect to find rule '{0}:{1}', but did", _rule.CheckId, _rule.Name);
            }

            if (conclusive)
            {
                Assert.AreEqual(0, found.Count, "Additional rules were triggered that weren't defined in the test.");
            }
        }

        private void TryLoadCodeByName()
        {
            MethodBase method = TestFrameworkHelpers.FindTestMethod();
            string filename = method.DeclaringType.Namespace + "." + method.DeclaringType.Name + "." + method.Name + ".cs";

            DeploymentItemAttribute deploymentItem = method.GetCustomAttributes(typeof(DeploymentItemAttribute), false).FirstOrDefault() as DeploymentItemAttribute;
            if (deploymentItem == null)
            {
                Assert.Fail("Your test method most use the 'DeploymentItem' attribute to ensure the source file is available to the Test.");
            }
            else if (!string.Equals(deploymentItem.Path, filename, StringComparison.OrdinalIgnoreCase))
            {
                Assert.Fail("Your external source file must have the following name for it to be loaded by the Test: '{0}'", filename);
            }
            else if (!File.Exists(filename))
            {
                Assert.Fail("External test file not found, make sure 'Deployment' is activated and that you've marked the file as 'Content' and 'Copy Always' in the file properties.");
            }

            OnCode(File.ReadAllText(filename));
        }
    }
}
